import { AccountInfoResponse, Client, Wallet as XrplWallet } from "xrpl";
import { Wallet as EthersWallet } from "@ethersproject/wallet";
import { JsonRpcProvider } from "@ethersproject/providers";
import { claimIDToHex, claimIDToNum, sleep, xrpToDrops } from "./utils";
import { ethers } from "ethers";
import { BridgeDoorToken, BridgeDoorToken__factory, BridgeToken, BridgeToken__factory } from "@peersyst/xrp-evm-contracts";
import { BridgeConfig } from "./BridgeConfig";
import { evmAddressToXrplAccount, xrplAccountToEvmAddress } from "../util/address-derivation";

export interface ChainProvider {
    init(): Promise<void>;
    getMasterAddress(): string;
    getMasterPrivateKey(): string;
    getBalance(address: string): Promise<number>;
    getBlock(): Promise<number>;
    getAddressFromPrivateKey(privateKey: string): string;
    getOtherChainAddress(address: string): string;
    fundAccount(pk: string, value: number): Promise<void>;
    refundAccount(pk: string): Promise<void>;
    createClaim(pk: string, senderAddress: string): Promise<number>;
    createAccount(pk: string, amount: number, receiverAddress: string): Promise<void>;
    commit(pk: string, claimID: number, amount: number, receiver?: string): Promise<void>;
    approve(pk: string, amount: number): Promise<void>;
    isClaimed(claimId: number, creator: string): Promise<boolean>;
    isCreated(receiver: string): Promise<boolean>;
}

export class XrpProvider implements ChainProvider {
    client: Client;
    masterNonce!: number;

    constructor(private bridgeConfig: BridgeConfig) {
        this.client = new Client(this.chainConfig.nodeUrl);
    }

    get chainConfig() {
        return this.bridgeConfig.bridgeChain("xrp");
    }
    get feePrivateKey() {
        return this.chainConfig.feePrivateKey;
    }
    get XChainBridge() {
        return this.chainConfig.XChainBridge;
    }
    get bridgeParams() {
        return this.bridgeConfig.config.params;
    }

    async init(): Promise<void> {
        await this.client.connect();
        const masterWallet = XrplWallet.fromSeed(this.feePrivateKey);
        const accountInfo: AccountInfoResponse = await this.client.request({
            command: "account_info",
            account: masterWallet.address,
        });
        this.masterNonce = accountInfo.result.account_data.Sequence;
    }

    getMasterAddress(): string {
        const masterWallet = XrplWallet.fromSeed(this.feePrivateKey);
        return masterWallet.address;
    }

    getMasterPrivateKey(): string {
        return this.feePrivateKey;
    }

    getOtherChainAddress(address: string): string {
        return xrplAccountToEvmAddress(address);
    }

    getAddressFromPrivateKey(privateKey: string): string {
        const masterWallet = XrplWallet.fromSeed(privateKey);
        return masterWallet.address;
    }

    async fundAccount(seed: string, value: number): Promise<void> {
        const masterWallet = XrplWallet.fromSeed(this.feePrivateKey);
        const wallet = XrplWallet.fromSeed(seed);

        const nonce = Number(this.masterNonce);
        console.log(`Funding account ${wallet.address} using nonce ${nonce}`);
        this.masterNonce++;

        const transaction = await this.client.autofill({
            TransactionType: "Payment",
            Destination: wallet.address,
            Account: masterWallet.address,
            Amount: xrpToDrops(value + 10),
            Sequence: nonce,
        });
        const signed = masterWallet.sign(transaction);
        await this.client.submit(signed.tx_blob);
    }

    // eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
    async refundAccount(seed: string): Promise<void> {}

    async createClaim(seed: string, senderAddress: string): Promise<number> {
        const wallet = XrplWallet.fromSeed(seed);
        const transaction = await this.client.autofill({
            TransactionType: "XChainCreateClaimID",
            XChainBridge: this.XChainBridge,
            SignatureReward: xrpToDrops(this.bridgeParams.signatureReward),
            OtherChainSource: senderAddress,
            Account: wallet.address,
        });
        const signed = wallet.sign(transaction);
        const result = await this.client.submit(signed.tx_blob);

        let claimID: any;
        do {
            await sleep(10);
            const response = await this.client.request({
                command: "tx",
                transaction: result.result.tx_json.hash,
                binary: false,
            });
            claimID = (response.result as any).meta.AffectedNodes.find((n: any) => n.CreatedNode?.LedgerEntryType === "XChainOwnedClaimID");
        } while (!claimID);
        const claimIDHex = claimID.CreatedNode.NewFields.XChainClaimID;
        return claimIDToNum(claimIDHex);
    }

    async createAccount(seed: string, amount: number, receiverAddress: string): Promise<void> {
        const wallet = XrplWallet.fromSeed(seed);
        const transaction = await this.client.autofill({
            TransactionType: "XChainAccountCreateCommit",
            XChainBridge: this.XChainBridge,
            SignatureReward: xrpToDrops(this.bridgeParams.signatureReward),
            Destination: receiverAddress,
            Amount: xrpToDrops(amount),
            Account: wallet.address,
        });
        const signed = wallet.sign(transaction);
        await this.client.submit(signed.tx_blob);
    }

    async commit(senderSeed: string, claimID: number, amount: number, receiver?: string): Promise<void> {
        const senderWallet = XrplWallet.fromSeed(senderSeed);
        const transaction = await this.client.autofill({
            TransactionType: "XChainCommit",
            XChainBridge: this.XChainBridge,
            XChainClaimID: claimIDToHex(claimID),
            OtherChainDestination: receiver,
            Amount: this.bridgeConfig.parseXrpAmount(amount),
            Account: senderWallet.address,
        });
        const signed = senderWallet.sign(transaction);
        const res = await this.client.submit(signed.tx_blob);
        console.log(res);
        console.log(transaction);
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
    async approve(pk: string, amount: number): Promise<void> {}

    async getBalance(address: string): Promise<number> {
        try {
            const balance = await this.client.getXrpBalance(address);
            return Number(balance);
        } catch (e) {
            return 0;
        }
    }

    async getBlock(): Promise<number> {
        return this.client.getLedgerIndex();
    }

    async isClaimed(claimId: number, creator: string, previouslyFound = false, checkIteration = 0): Promise<boolean> {
        try {
            const res = await this.client.request({
                command: "account_objects",
                account: creator,
            });
            if (res.result.account_objects.length === 0) {
                if (previouslyFound || checkIteration >= 10) return true;
                else {
                    await sleep(0.2);
                    return this.isClaimed(claimId, creator, false, checkIteration + 1);
                }
            }
            for (const obj of res.result.account_objects as any) {
                if (obj.XChainClaimID.toUpperCase() === claimId.toString(16).toUpperCase()) {
                    return await this.isClaimed(claimId, creator, true, 0);
                }
            }
            return false;
        } catch (e) {
            console.log(`Error when trying to find if claimId ${claimId} is claimed for ${creator}: ${e}`);
            return false;
        }
    }

    async isCreated(receiver: string): Promise<boolean> {
        try {
            await this.client.request({
                command: "account_info",
                account: receiver,
            });
            return true;
        } catch (e) {
            return false;
        }
    }
}

export class EvmProvider implements ChainProvider {
    provider: JsonRpcProvider;
    bridgeContract!: BridgeDoorToken;
    tokenContract!: BridgeToken;
    feeAccountNonce!: number;
    fundedPrivateKeys: string[] = [];

    constructor(private bridgeConfig: BridgeConfig) {
        this.provider = new JsonRpcProvider(this.chainConfig.nodeUrl);
    }

    get chainConfig() {
        return this.bridgeConfig.bridgeChain("evm");
    }
    get feePrivateKey() {
        return this.chainConfig.feePrivateKey;
    }
    get bridgeParams() {
        return this.bridgeConfig.config.params;
    }

    async init(): Promise<void> {
        this.bridgeContract = BridgeDoorToken__factory.connect(this.chainConfig.bridgeAccount, this.provider);
        this.tokenContract = BridgeToken__factory.connect(this.chainConfig.tokenAddress, this.provider);
        const masterWallet = new EthersWallet(this.feePrivateKey, this.provider);
        this.feeAccountNonce = await this.provider.getTransactionCount(masterWallet.address);
    }

    getMasterAddress(): string {
        const masterWallet = new EthersWallet(this.feePrivateKey, this.provider);
        return masterWallet.address;
    }

    getMasterPrivateKey(): string {
        return this.feePrivateKey;
    }

    getOtherChainAddress(address: string): string {
        return evmAddressToXrplAccount(address);
    }

    getAddressFromPrivateKey(privateKey: string): string {
        const masterWallet = new EthersWallet(privateKey, this.provider);
        return masterWallet.address;
    }

    async fundAccount(privateKey: string, value: number): Promise<void> {
        const masterWallet = new EthersWallet(this.feePrivateKey, this.provider);
        const wallet = new EthersWallet(privateKey, this.provider);

        const nonce = Number(this.feeAccountNonce);
        console.log(`Funding account ${wallet.address} using nonce ${nonce}`);
        this.feeAccountNonce++;
        for (let i = 0; i < Infinity; i++) {
            try {
                await masterWallet.sendTransaction({
                    to: wallet.address,
                    value: ethers.utils.parseEther(value.toString()),
                    nonce: nonce,
                });
                break;
            } catch (e: any) {
                console.log(`Errored trying to broadcast fund transaction with nonce ${nonce}: ${e.body}`);
                await sleep(0.2);
            }
        }
    }

    async refundAccount(pk: string): Promise<void> {
        const masterWallet = new EthersWallet(this.feePrivateKey, this.provider);
        const wallet = new EthersWallet(pk, this.provider);
        const walletBalance = await this.getBalance(wallet.address);
        const valueToSend = walletBalance - 0.1;
        if (valueToSend <= 0) return;
        console.log(`Sending back ${valueToSend} from ${wallet.address}`);
        await (
            await wallet.sendTransaction({
                to: masterWallet.address,
                value: ethers.utils.parseEther(valueToSend.toString()),
                gasLimit: 21_000,
            })
        ).wait();
    }

    async createAccount(pk: string, amount: number, receiverAddress: string): Promise<void> {
        const wallet = new EthersWallet(pk, this.provider);
        const signatureReward = ethers.utils.parseEther(this.bridgeParams.signatureReward.toString());
        const parsedAmount = ethers.utils.parseEther(amount.toString());
        await (
            await this.bridgeContract.connect(wallet).createAccountCommit(receiverAddress, parsedAmount, signatureReward, {
                value: parsedAmount.add(signatureReward),
                gasLimit: 300_000,
            })
        ).wait();
    }

    async createClaim(pk: string, senderAddress: string): Promise<number> {
        const wallet = new EthersWallet(pk, this.provider);
        const contractTransaction = await this.bridgeContract.connect(wallet).createClaimId(senderAddress, {
            value: ethers.utils.parseEther(this.bridgeParams.signatureReward.toString()),
            gasLimit: 140_000,
        });
        const transaction = await contractTransaction.wait();
        const event = transaction.events?.find((event: any) => event.event === "CreateClaim");
        const [claimID] = event?.args || [];
        return claimID.toNumber();
    }

    async commit(senderPk: string, claimID: number, amount: number, receiver?: string): Promise<void> {
        const wallet = new EthersWallet(senderPk, this.provider);
        const parsedAmount = ethers.utils.parseEther(amount.toString());
        if (receiver) {
            await (
                await this.bridgeContract.connect(wallet).commit(receiver, claimID, parsedAmount, {
                    value: this.bridgeConfig.isTokenBridge ? undefined : parsedAmount,
                    gasLimit: 100_000,
                })
            ).wait();
        } else {
            await (
                await this.bridgeContract.connect(wallet).commitWithoutAddress(claimID, parsedAmount, {
                    value: this.bridgeConfig.isTokenBridge ? undefined : parsedAmount,
                    gasLimit: 100_000,
                })
            ).wait();
        }
    }

    async approve(senderPk: string, amount: number): Promise<void> {
        const wallet = new EthersWallet(senderPk, this.provider);
        const parsedAmount = ethers.utils.parseEther(amount.toString());
        await (await this.tokenContract.connect(wallet).approve(this.chainConfig.bridgeAccount, parsedAmount)).wait();
    }

    async getBalance(address: string): Promise<number> {
        const balance = await this.provider.getBalance(address);
        return Number(ethers.utils.formatEther(balance));
    }

    async getBlock(): Promise<number> {
        return this.provider.getBlockNumber();
    }

    async getLatestBlock(): Promise<number> {
        return this.provider.getBlockNumber();
    }

    async isClaimed(claimId: number): Promise<boolean> {
        const res = await this.bridgeContract.queryFilter(
            this.bridgeContract.filters.Credit(claimId),
            (await this.provider.getBlockNumber()) - 3000,
        );
        return res.length > 0;
    }

    async isCreated(receiver: string): Promise<boolean> {
        const res = await this.bridgeContract.queryFilter(
            this.bridgeContract.filters.CreateAccount(receiver),
            (await this.provider.getBlockNumber()) - 3000,
        );
        return res.length > 0;
    }
}
